﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Epsilon.Typing;
using Epsilon.Expr;

namespace Epsilon.Output
{
    [Serializable]
    public class OutputException : Exception
    {
        public OutputException() { }
        public OutputException(string message) : base(message) { }
        public OutputException(string message, Exception inner) : base(message, inner) { }
        protected OutputException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context)
            : base(info, context) { }
    }

    public partial class Writer
    {
        const string INDENT = "  ";
        Process process = null;
        string indent = "";
        StreamWriter writer = null;
        bool newLine = true;
        int col = 0;
        int var = 0;

        public VarExpr NewVar()
        {
            var++;
            return new VarExpr("__X" + var.ToString(), -1, -1);
        }

        public Writer(Process p)
        {
            process = p;
        }

        public void Write()
        {
            ComputeRxGuards();
            int extDot = process.Filename.LastIndexOf('.');
            string filename = process.Filename.Substring(0, extDot) + ".erl";
            Console.WriteLine("Writing file " + filename);

            writer = new StreamWriter(filename);
            indent = "";

            try
            {
                // write information about the process
                OutputNewline();
                OutputNewline("% Original file: " + process.Filename);

                if (process.Specs.Count > 0)
                {
                    OutputNewline();

                    foreach (KeyValuePair<string, TypeLabel> kvp in process.Specs)
                        WriteSpec(kvp.Key, kvp.Value);
                }

                OutputNewline();
                OutputNewline("-process(" + process.Name + ").");
                OutputNewline("-module(" + process.Name + ").");
                OutputNewline("-export([main/0]).");

                if (process.Records.Records.Count > 0)
                {
                    OutputNewline();

                    foreach (string n in process.Records.Records.Keys)
                        WriteRecordDef(n, process.Records.Records[n]);
                }

                if (process.Attributes.Count > 0)
                {
                    OutputNewline();

                    for (int i = 0; i < process.Attributes.Count; i++)
                        WriteAttribute(process.Attributes[i]);
                }

                OutputNewline();
                OutputFunctions();
            }
            catch (Exception e)
            {
                throw e;
            }
            finally
            {
                writer.Close();
            }
        }

        public void Output(string s)
        {
            if (newLine)
            {
                writer.Write(indent);
                col += indent.Length;
                newLine = false;
            }
            writer.Write(s);
            col += s.Length;
        }

        public void OutputNewline()
        {
            writer.WriteLine();
            newLine = true;
            col = 0;
        }

        public void OutputNewline(string s)
        {
            Output(s);
            OutputNewline();
        }

        public void SetIndent(int i)
        {
            indent = "";
            for (int j = 0; j < i; j++)
                indent += " ";
        }

        public void IncreaseIndent()
        {
            indent += INDENT;
        }

        public void DecreaseIndent()
        {
            indent = indent.Substring(0, indent.Length - INDENT.Length);
        }

        public string FromAtom(string a)
        {
            bool unquoted = true;
            char[] cs = a.ToCharArray();

            if (cs.Length == 0)
                unquoted = false;
            else if (cs[0] < 'a' || cs[0] > 'z')
                unquoted = false;

            // check each char is allowed
            for (int c = 0; c < cs.Length; c++)
            {
                if (Array.IndexOf(AtomExpr.CHARS, cs[c]) < 0)
                {
                    unquoted = false;
                    break;
                }
            }

            // check for keywords
            if (Array.IndexOf(AtomExpr.KEYS, a) >= 0)
                unquoted = false;

            if (unquoted)
                return a;
            else
                return "'" + a + "'";
        }

        public void WriteFunctions(List<FunctionExpr> fs)
        {
            for (int i = 0; i < fs.Count; i++)
            {
                Write(fs[i]);
                if (i < fs.Count - 1)
                    OutputNewline(";");
                else
                    OutputNewline(".");
            }
            OutputNewline();
        }

        public void ComputeRxGuards()
        {
            List<RxClauseExpr> rxs = new List<RxClauseExpr>();
            rxs.AddRange(process.RxStore.GetAllClauses());

            while (rxs.Count > 0)
            {
                RxClauseExpr e = rxs[0];

                VarExpr pv = e.Pattern as VarExpr;
                if (e.Type == SimpleType.BoolType)
                {
                    e.Guard = pv.Var + " =:= true ; " + pv.Var + " =:= false";
                    rxs.Remove(e);
                    continue;
                }
                else if (e.Type == SimpleType.IntType)
                {
                    e.Guard = "is_integer(" + pv.Var + ")";
                    rxs.Remove(e);
                    continue;
                }
                else if (e.Type == SimpleType.PidType)
                {
                    e.Guard = "is_pid(" + pv.Var + ")";
                    rxs.Remove(e);
                    continue;
                }
                else if (e.Type == SimpleType.StringType)
                {
                    for (int i = 0; i < rxs.Count; i++)
                    {
                        if (rxs[i].Type is ListType)
                            throw new OutputException(
                                "Unable to distinguish between a receive clause" +
                                " receiving a string and a list");
                    }
                    e.Guard = "is_list(" + pv.Var + ")";
                    rxs.Remove(e);
                    continue;
                }
                else if (e.Type == SimpleType.UnitType)
                {
                    e.Guard = pv.Var + " =:= void";
                    rxs.Remove(e);
                    continue;
                }
                else if (e.Type is AtomType)
                {
                    e.Guard = pv.Var + " =:= " + FromAtom(((AtomType)e.Type).Value);
                    rxs.Remove(e);
                    continue;
                }
                else if (e.Type is FunType)
                {
                    for (int i = 0; i < rxs.Count; i++)
                    {
                        if (rxs[i].Type is FunType && rxs[i].Type != e.Type)
                            throw new OutputException(
                                "Unable to distinguish between two receive clauses" +
                                "requiring functions of differing types");
                    }
                    e.Guard = "is_function(" + pv.Var + ")";
                    rxs.Remove(e);
                    continue;
                }
                else if (e.Type is ListType)
                {
                    for (int i = 0; i < rxs.Count; i++)
                    {
                        if (rxs[i].Type == SimpleType.StringType ||
                            ((rxs[i].Type is ListType) &&
                            ((ListType)rxs[i].Type).ElementType == ((ListType)e.Type).ElementType))
                            throw new OutputException(
                                "Unable to distinguish between two receive clauses" +
                                "requiring a list and a string or two different list types");
                    }
                    e.Guard = "is_list(" + pv.Var + ")";
                    rxs.Remove(e);
                    continue;
                }
                else if (e.Type is TupleType)
                {
                    TupleType et = e.Type as TupleType;
                    bool error = false;

                    // ignore first one
                    for (int i = 1; i < rxs.Count; i++)
                    {
                        bool valid = false;
                        TupleType rxt = rxs[i].Type as TupleType;

                        if (rxt == null || rxt.Types.Count != et.Types.Count)
                            continue;

                        // Must not equal on atom types at the top level
                        for (int j = 0; j < et.Types.Count; j++)
                        {
                            if (et.Types[j] != rxt.Types[j] && 
                                et.Types[j] is AtomType && rxt.Types[j] is AtomType &&
                                ((AtomType)et.Types[j]) != ((AtomType)rxt.Types[j]))
                            {
                                valid = true;
                                break;
                            }
                        }

                        if (!valid)
                            error = true;
                    }

                    if (error)
                        throw new OutputException("Receive tuples must differ on atoms");

                    // now compute the guard
                    string g = "";

                    if (pv == null)
                    {
                        pv = NewVar();
                        g = e.Pattern.ToString() + " = " + pv.ToString() + ", ";
                    }

                    g += "is_tuple(" + pv.Var + "), " + "size(" + pv.Var + ") =:= " + et.Types.Count.ToString();

                    for (int i = 0; i < et.Types.Count; i++)
                    {
                        AtomType at = et.Types[i] as AtomType;
                        if (at == null)
                            continue;

                        g += ", element(" + (i + 1).ToString() + ", " + pv.Var +
                            ") =:= " + FromAtom(at.Value);
                    }
                    e.Guard = g;
                    rxs.Remove(e);
                    continue;
                }
                else
                    throw new OutputException("Unknown type encountered");
            }
        }

        public void OutputFunctions()
        {
            List<FunctionExpr> fs = new List<FunctionExpr>();
            fs.AddRange(process.Functions);

            List<FunctionExpr> ffs = new List<FunctionExpr>();
            while (fs.Count > 0)
            {
                ffs.Clear();
                ffs.Add(fs[0]);

                for (int i = 1; i < fs.Count; i++)
                {
                    if (fs[i].Name == fs[0].Name &&
                        fs[i].Arguments.Count == fs[0].Arguments.Count)
                        ffs.Add(fs[i]);
                }

                if (ffs.Count > 1)
                    OutputFunctions(ffs);
                else
                    WriteFunctions(ffs);

                for (int i = 0; i < ffs.Count; i++)
                    fs.Remove(ffs[i]);
            }
        }

        public void OutputFunctions(List<FunctionExpr> fs)
        {
            // output functions with the same name and arity

            // for each function
            for (int i = 0; i < fs.Count; i++)
            {
                bool valid = false;

                // for each argument in that function
                for (int a = 0; a < fs[i].Arguments.Count; a++)
                {
                    bool unique = true;

                    // for each other function
                    for (int j = 0; j < fs.Count; j++)
                    {
                        if (j == i)
                            continue;

                        // check if the argument is unique
                        if (Same(fs[i].Arguments[a].Type, fs[j].Arguments[a].Type))
                        {
                            unique = false;
                            break;
                        }
                    }

                    // if it is unique then use this as the guard
                    if (unique)
                    {
                        fs[i].Guard = CreateGuard(fs[i].Arguments[a]);
                        valid = true;
                        break;
                    }
                }

                if (!valid)
                    throw new OutputException("Unable to distinguish between two functions with the same name and arity: " + fs[0].Name + "/" + fs[0].Arguments.Count);
            }

            // finally we can write the list of functions
            WriteFunctions(fs);
        }

        public bool Same(Type t, Type u)
        {
            if (t == u)
                return true;

            if (t == SimpleType.BoolType && u != SimpleType.BoolType)
                return false;

            if (t == SimpleType.IntType && u != SimpleType.IntType)
                return false;

            if (t == SimpleType.UnitType && u != SimpleType.UnitType)
                return false;

            if (t == SimpleType.StringType && u != SimpleType.StringType && !(u is ListType))
                return false;

            if ((t is ListType) && u != SimpleType.StringType && !(u is ListType))
                return false;

            // we know u is not equal to t here, and atoms are only equal if their types are
            if (t is AtomType)
                return false;

            return true;
        }

        public string CreateGuard(FunctionArg arg)
        {
            string g = "";

            VarExpr pv = arg.Pattern as VarExpr;
            if (pv == null)
            {
                pv = NewVar();
                g = arg.Pattern.ToString() + " = " + pv.Var + ", ";
            }

            if (arg.Type == SimpleType.BoolType)
                g += pv.Var + " =:= true ; " + pv.Var + " =:= false";
            else if (arg.Type == SimpleType.IntType)
                g += "is_integer(" + pv.Var + ")";
            else if (arg.Type == SimpleType.UnitType)
                g += pv.Var + " =:= void";
            else if (arg.Type == SimpleType.StringType || arg.Type is ListType)
                g += "is_list(" + pv.Var + ")";
            else if (arg.Type is AtomType)
                g += pv.Var + " =:= " + FromAtom(((AtomType)arg.Type).Value);
            else
                throw new OutputException("Unable to create function guard");

            return g;
        }
    }
}
